package perl.compiler;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import perl.compiler.utils.Node;
import perl.lexical.classes.Delimeter;
import perl.lexical.classes.Keyword;
import perl.lexical.classes.Operator;
import perl.lexical.classes.Token;
import perl.lexical.classes.TokenClassEnum;


// 1. FIX output of error (row and column)

public class SyntaxAnalizer {

	Map<Integer, Token> tokens;
	private int tokenId = 1;
	private int nestingLevel;
	Node<Token> tree = new Node<Token>(null, null);
	List<String> syntaxLog = new ArrayList<String>();
	
	public SyntaxAnalizer(Map<Integer, Token> tokens) {
		this.tokens = tokens;
	}
	
	
	// Should be Map of Tokens for input;
	public Node<Token> analize() throws Exception {
		//System.out.println("-----------------------------\n");
		if(tokens == null){
			throw new Exception("ERORR: There are no tokens to analize");
		}
		nestingLevel = 0;
		// go through the program
		while(E() && tokenId <= tokens.size());
		//System.out.println("Structure 100 is parsed");
		return new Node<Token>(null,null);

	}

	boolean E() throws Exception {

		return abstractTerminal();
	}

	boolean abstractTerminal() throws Exception {
		if(procedureDeclaration() || statementDeclaration() || variableDeclaration() || expressionDeclaration()){
			return true;
		}else{
			throw new Exception("Erorr while parsing: " + tokens.get(tokenId));
			//System.out.println("Erorr while parsing: " + tokens.get(tokenId));
		}
		
	}

	// sub sub_name { ... };
	// ; - is optional in the end
	boolean procedureDeclaration() {

		final Token SUB = new Token(TokenClassEnum.KEYWORD, Keyword.SUB);
		final TokenClassEnum ANY_IDENTIFIER = TokenClassEnum.IDENTIFIER;
		nestingLevel = 1;
		syntaxLog.add("Rule #1 is used");
		int savedTokenId = this.tokenId;
		if (nonTerm(SUB) && nonTerm(ANY_IDENTIFIER)
				&& statementDeclaration()) {		
			syntaxLog.add("Structure 107 is parsed");
			return true;
		}
		
		// if don't match return pointer to the start token
		this.tokenId = savedTokenId;
		nestingLevel = 0;
		return false;
	}

	// empty statement - { }; or full statement - { variables + expressions };
	// ; - is optional
	boolean statementDeclaration() {
		
		int savedTokenId = this.tokenId;
		syntaxLog.add("Rule #7 is used");
		if (emptyStatement() || fullStatement()) {
			syntaxLog.add("Structure 102 is parsed");
			return true;
		}
		
		// if don't match return pinter to the start token
		this.tokenId = savedTokenId;
		return false;
	}
	
	//empty Statement{}
	boolean emptyStatement(){
		final Token LBRA = new Token(TokenClassEnum.DELIMETER, Delimeter.LBRA); // {
		final Token RBRA = new Token(TokenClassEnum.DELIMETER, Delimeter.RBRA); // }
		int savedTokenId = this.tokenId;
		
		boolean EMPTY_STATEMENT = nonTerm(LBRA) && nonTerm(RBRA);
		if(EMPTY_STATEMENT){
			return true;
		}
		
		this.tokenId = savedTokenId;
		return false;
	}
	
	//full Statement {...}
	boolean fullStatement(){
		final Token LBRA = new Token(TokenClassEnum.DELIMETER, Delimeter.LBRA); // {
		final Token RBRA = new Token(TokenClassEnum.DELIMETER, Delimeter.RBRA); // }
		final Token SEMICOLON = new Token(TokenClassEnum.DELIMETER, Delimeter.SEMICOLON); // ;
		final Token RETURN = new Token(TokenClassEnum.KEYWORD, Keyword.RETURN);
		final TokenClassEnum SCALAR = TokenClassEnum.SCALAR_IDENT; // $string
		int savedTokenId = this.tokenId;
		
		if(nonTerm(LBRA)){
			// look through the statement
			while(variableDeclaration() || expressionDeclaration() );
			//}
			if(optional(nonTerm(RETURN) && 
							(nonTerm(SCALAR) || nonTerm(TokenClassEnum.SCALAR_IDENT) || (nonTerm(TokenClassEnum.PSTRING) || nonTerm(TokenClassEnum.PNUMBER))) && nonTerm(SEMICOLON)) &&
							nonTerm(RBRA) && optional(SEMICOLON)){
				return true;
			}
		}
				
		this.tokenId = savedTokenId;
		return false;
	}
		
	// ALL VARIABLES HAVE TO BE INITIALIZED IMMEDIATELY! (perl feature...)
	// $b = 5 or $b = $c ONLY OK
	// $b; BAD
	boolean variableDeclaration() {

		final TokenClassEnum SCALAR = TokenClassEnum.SCALAR_IDENT; // $string
		final TokenClassEnum ARRAY = TokenClassEnum.ARRAY_IDENT; // @array
		final Token EQUAL = new Token(TokenClassEnum.OPERATOR, Operator.ASSIGN); // =
		final Token SEMICOLON = new Token(TokenClassEnum.DELIMETER, Delimeter.SEMICOLON); // ;
		final Token LPAR = new Token(TokenClassEnum.DELIMETER, Delimeter.LPAR); // (
		final Token RPAR = new Token(TokenClassEnum.DELIMETER, Delimeter.RPAR); // (
		boolean PARRAY = false;

		int savedTokenId = this.tokenId;
		syntaxLog.add("Rule #2 is used");
		// SCALAR or NUMBER or STRING
		boolean PSTR_NUM = nonTerm(SCALAR) && nonTerm(EQUAL) && 
				(nonTerm(TokenClassEnum.PSTRING) || nonTerm(TokenClassEnum.PNUMBER)) && nonTerm(SEMICOLON);
		if(PSTR_NUM != true){
			PARRAY = nonTerm(ARRAY) && nonTerm(EQUAL) && nonTerm(LPAR) && arrayInitElements() && nonTerm(RPAR) 
				&&	nonTerm(SEMICOLON);
		}
		if (PSTR_NUM || PARRAY){ // ARRAY HAVE TO BE ADDED!!!!!
			syntaxLog.add("Structure 104 is parsed.");
			// mark this variable as declarated
			tokens.get(savedTokenId).setDeclarated(true);
			tokens.get(savedTokenId).setNestingLevel(nestingLevel);
			//tokens.get(arrayId).setDeclarated(true);
			return true;
		}
		
		// if don't match return pinter to the start token
		this.tokenId = savedTokenId;
		return false;
	}
	
	boolean arrayInitElements(){
		
		int savedTokenId = this.tokenId;
		final TokenClassEnum SCALAR = TokenClassEnum.SCALAR_IDENT; // $string
		final Token COMMA = new Token(TokenClassEnum.DELIMETER, Delimeter.COMMA); // ;
		
		if (nonTerm(SCALAR) || nonTerm(TokenClassEnum.PSTRING) || nonTerm(TokenClassEnum.PNUMBER)){ // ARRAY HAVE TO BE ADDED!!!!!
			while(nonTerm(COMMA) && (nonTerm(SCALAR) || nonTerm(TokenClassEnum.PSTRING) || nonTerm(TokenClassEnum.PNUMBER)));
			return true;
		}
		
		// if don't match return pinter to the start token
		this.tokenId = savedTokenId;
		return false;
	}
	// for , while, goto ....
	boolean expressionDeclaration() {
		
		int savedTokenId = this.tokenId;	
		syntaxLog.add("Rule #4 is used");
		if(variableDeclaration() || 
		   forExpression() || 
		   whileExpression() ||
		   ifExpression() ||
		   assignmentExpression() ||
		   incDecExpression() ||
		   printExpression() ||
		   nextLastExpression()){
			
			return true;
		}
		
		// if don't match return pinter to the start token
		this.tokenId = savedTokenId;
		return false;
	}

	//---------------------------------EXPRESSIONS---------------------
	
	boolean callFunctionExpression(){
		int savedTokenId = this.tokenId;
		final TokenClassEnum ANY_IDENTIFIER = TokenClassEnum.IDENTIFIER;
		final Token LPAR = new Token(TokenClassEnum.DELIMETER, Delimeter.LPAR); // (
		final Token RPAR = new Token(TokenClassEnum.DELIMETER, Delimeter.RPAR); // (
		
		if(nonTerm(ANY_IDENTIFIER) && nonTerm(LPAR) && nonTerm(RPAR)){
			if(tokens.get(savedTokenId-2).isDeclarated() == false){
				tokens.get(savedTokenId-2).setDeclarated(true);
			}
			return true;
		}
		
		// if don't match return pinter to the start token
		this.tokenId = savedTokenId;
		return false;
	}
	
	// $a = $b + $c
	boolean assignmentExpression(){
		int savedTokenId = this.tokenId;	
		final TokenClassEnum SCALAR = TokenClassEnum.SCALAR_IDENT;
		final Token EQUAL = new Token(TokenClassEnum.OPERATOR, Operator.ASSIGN); // =
		final Token SEMICOLON = new Token(TokenClassEnum.DELIMETER, Delimeter.SEMICOLON); // ;
		
		if(nonTerm(SCALAR) && nonTerm(EQUAL) && (calculationExpression() || callFunctionExpression()) && nonTerm(SEMICOLON)){
			
			return true;
		}
		
		// if don't match return pinter to the start token
		this.tokenId = savedTokenId;
		return false;
	}
	
	
	private boolean calculationExpression() {
		int savedTokenId = this.tokenId;	
		final TokenClassEnum SCALAR = TokenClassEnum.SCALAR_IDENT;
		final Token PLUS = new Token(TokenClassEnum.OPERATOR, Operator.PLUS);
		final Token MINUS = new Token(TokenClassEnum.OPERATOR, Operator.MINUS);
		final Token MUL = new Token(TokenClassEnum.OPERATOR, Operator.MULTIPLY);
		final Token DEV = new Token(TokenClassEnum.OPERATOR, Operator.DIVIDE);
		final Token INC = new Token(TokenClassEnum.OPERATOR, Operator.INCREASE);
		final Token DEC = new Token(TokenClassEnum.OPERATOR, Operator.DECREASE);
		
		if(nonTerm(SCALAR) || nonTerm(TokenClassEnum.SCALAR_IDENT) || (nonTerm(TokenClassEnum.PSTRING) || nonTerm(TokenClassEnum.PNUMBER))){
			while((nonTerm(PLUS) || nonTerm(MINUS) || nonTerm(MUL) || nonTerm(DEV) || nonTerm(INC) || nonTerm(DEC)) 
				&& (nonTerm(SCALAR) || nonTerm(TokenClassEnum.SCALAR_IDENT) || (nonTerm(TokenClassEnum.PSTRING) || nonTerm(TokenClassEnum.PNUMBER))));
			return true;
		}
		
		// if don't match return pinter to the start token
		this.tokenId = savedTokenId;
		return false;
	}


	// for ($digit = 0; $digit <=10; $digit++)
	boolean forExpression(){
		
		final Token FOR = new Token(TokenClassEnum.KEYWORD, Keyword.FOR);
		final Token LPAR = new Token(TokenClassEnum.DELIMETER, Delimeter.LPAR); // (
		final Token RPAR = new Token(TokenClassEnum.DELIMETER, Delimeter.RPAR); // (
		final Token SEMICOLON = new Token(TokenClassEnum.DELIMETER, Delimeter.SEMICOLON); // ;
		final Token LESS = new Token(TokenClassEnum.OPERATOR, Operator.LESS); // <
		final Token LESS_EQUAL = new Token(TokenClassEnum.OPERATOR, Operator.LESSorEQ); // <=
		final TokenClassEnum SCALAR = TokenClassEnum.SCALAR_IDENT; // $string
		final Token INCREASE = new Token(TokenClassEnum.OPERATOR, Operator.INCREASE); // ++
		int savedTokenId = this.tokenId;
		syntaxLog.add("Rule #11 is used");
		boolean FOR_EXPRESSION = 
				//for ( $digit = 0;
				nonTerm(FOR) && nonTerm(LPAR) && variableDeclaration()
				//$digit <=10;
				&& nonTerm(SCALAR) && (nonTerm(LESS) || nonTerm(LESS_EQUAL)) 
				&& (nonTerm(TokenClassEnum.SCALAR_IDENT) || (nonTerm(TokenClassEnum.PSTRING) || nonTerm(TokenClassEnum.PNUMBER))) 
				&& nonTerm(SEMICOLON) 
				//$digit++)
				&& nonTerm(SCALAR) && nonTerm(INCREASE) && nonTerm(RPAR)
				//{}
				&& statementDeclaration()
				;
		
		if(FOR_EXPRESSION){
			syntaxLog.add("Structure 114 is parsed");
			return true;
		}
		
		this.tokenId = savedTokenId;
		return false;
	}
	
	// while ($digit != 0){}
	boolean whileExpression(){
		
		final Token WHILE = new Token(TokenClassEnum.KEYWORD, Keyword.WHILE);
		int savedTokenId = this.tokenId;
		syntaxLog.add("Rule #29 is used");
		boolean WHILE_EXPRESSION = 
				//while ( $digit == 0)
				nonTerm(WHILE) && conditionExpression()
				//{}
				&& statementDeclaration()
				;
		
		if(WHILE_EXPRESSION){
			syntaxLog.add("Structure 115 is parsed");
			return true;
		}
		
		this.tokenId = savedTokenId;
		return false;
	}
	
	boolean ifExpression(){
		final Token IF = new Token(TokenClassEnum.KEYWORD, Keyword.IF);
		final Token ELSE = new Token(TokenClassEnum.KEYWORD, Keyword.ELSE);
		int savedTokenId = this.tokenId;
		syntaxLog.add("Rule #27 is used");
		boolean IF_EXPRESSION = 
				//while ( $digit == 0)
				nonTerm(IF) && conditionExpression()
				//{}
				&& statementDeclaration()
				&& optional(nonTerm(ELSE) && statementDeclaration())
				;
		
		if(IF_EXPRESSION){
			syntaxLog.add("Structure 112 is parsed");
			return true;
		}
		
		this.tokenId = savedTokenId;
		return false;
		
	}
	//condition ($a != &b)
	boolean conditionExpression(){
		final Token LPAR = new Token(TokenClassEnum.DELIMETER, Delimeter.LPAR); // (
		final Token RPAR = new Token(TokenClassEnum.DELIMETER, Delimeter.RPAR); // (
		final TokenClassEnum SCALAR = TokenClassEnum.SCALAR_IDENT; // $string
		final Token LESS = new Token(TokenClassEnum.OPERATOR, Operator.LESS); // <
		final Token LESS_EQUAL = new Token(TokenClassEnum.OPERATOR, Operator.LESSorEQ); // <=
		final Token GREATER = new Token(TokenClassEnum.OPERATOR, Operator.GREATER); // >
		final Token GREATER_EQUAL = new Token(TokenClassEnum.OPERATOR, Operator.GRorEQ); // >=
		final Token NOT_EQUAL = new Token(TokenClassEnum.OPERATOR, Operator.NOTEQUAL); // !=
		final Token EQUAL = new Token(TokenClassEnum.OPERATOR, Operator.EQUAL); // ==
		int savedTokenId = this.tokenId;
		syntaxLog.add("Rule #22 is used");
		boolean CONDITION_EXPR = nonTerm(LPAR) &&
				(nonTerm(SCALAR) || nonTerm(TokenClassEnum.SCALAR_IDENT) || (nonTerm(TokenClassEnum.PSTRING) || nonTerm(TokenClassEnum.PNUMBER))) &&
					(nonTerm(LESS) || nonTerm(LESS_EQUAL) || nonTerm(GREATER) || nonTerm(GREATER_EQUAL) || nonTerm(NOT_EQUAL) || nonTerm(EQUAL)) &&
				(nonTerm(SCALAR) || nonTerm(TokenClassEnum.SCALAR_IDENT) || (nonTerm(TokenClassEnum.PSTRING) || nonTerm(TokenClassEnum.PNUMBER))) &&
								 nonTerm(RPAR);
				 
		
		if(CONDITION_EXPR){
			syntaxLog.add("Structure 120 is parsed");
			return true;
		}
		
		this.tokenId = savedTokenId;
		return false;
	}
	
	
	// INCREMENT and DECREMENT EXPRESSION
	boolean incDecExpression(){
		
		int savedTokenId = this.tokenId;
		final Token SEMICOLON = new Token(TokenClassEnum.DELIMETER, Delimeter.SEMICOLON); // ;
		final Token INCREASE = new Token(TokenClassEnum.OPERATOR, Operator.INCREASE); // ++
		final Token DECREASE = new Token(TokenClassEnum.OPERATOR, Operator.DECREASE); // --
		boolean INCDEC = (nonTerm(TokenClassEnum.PSTRING) || nonTerm(TokenClassEnum.PNUMBER) || nonTerm(TokenClassEnum.SCALAR_IDENT)) 
							&& (nonTerm(INCREASE) || nonTerm(DECREASE)) && nonTerm(SEMICOLON);
		syntaxLog.add("Rule #18 is used");
		if(INCDEC){
			syntaxLog.add("Structure 111 is parsed");
			return true;
		}
		
		this.tokenId = savedTokenId;
		return false;
	}
	
	boolean nextLastExpression(){	
		int savedTokenId = this.tokenId;
		final Token NEXT = new Token(TokenClassEnum.KEYWORD, Keyword.NEXT); // next
		final Token LAST = new Token(TokenClassEnum.KEYWORD, Keyword.LAST); // last
		final TokenClassEnum ANY_IDENTIFIER = TokenClassEnum.IDENTIFIER;
		final Token SEMICOLON = new Token(TokenClassEnum.DELIMETER, Delimeter.SEMICOLON); // ;
		
		boolean NEXT_LAST = ((nonTerm(NEXT) ||  nonTerm(LAST)) && optional(ANY_IDENTIFIER) && nonTerm(SEMICOLON)) ;

		if(NEXT_LAST){
			syntaxLog.add("Structure 116 is parsed");
			return true;
		}

		this.tokenId = savedTokenId;
		return false;
	}
	
	// PRINT EXPRESSION
	boolean printExpression(){
			
		final Token SEMICOLON = new Token(TokenClassEnum.DELIMETER, Delimeter.SEMICOLON); // ;
		final TokenClassEnum SCALAR = TokenClassEnum.SCALAR_IDENT; // $string	
		final Token PRINT = new Token(TokenClassEnum.KEYWORD, Keyword.PRINT); // ++
		int savedTokenId = this.tokenId;
		// PRINT print $afd; print 5;
		boolean PRINT_EXPRESSION = nonTerm(PRINT) && (nonTerm(SCALAR) || nonTerm(TokenClassEnum.SCALAR_IDENT) || nonTerm(TokenClassEnum.PSTRING) || nonTerm(TokenClassEnum.PNUMBER)) 
				&& nonTerm(SEMICOLON);
		if(PRINT_EXPRESSION){
			syntaxLog.add("Structure 122 is parsed");
			return true;
		}
		
		this.tokenId = savedTokenId;
		return false;
	}
	
	//---------------------------------------------------------
	
	boolean nonTerm(Token token) {
		if(tokenId <= tokens.size()){
			Token currentToken = tokens.get(tokenId++);
			
			if (currentToken.equals(token)) {
				//System.out.println(currentToken.getLexem());
				tree.setData(currentToken);
				return true;
			}
			// move pointer back to previous token if don't match!!!! 
			
			tokenId--;
		}
		return false;
	}

	boolean nonTerm(TokenClassEnum tokenClass) {
		
		if(tokenId <= tokens.size()){
			Token currentToken = tokens.get(tokenId++);
			TokenClassEnum curentTokenClass = currentToken.getTokenClass();
			
			if (curentTokenClass == tokenClass) { // ????????
				//System.out.println(currentToken.getLexem());
				return true;
			}
			
			// move pointer back to previous token!!!!
			
			tokenId--;
		}
		return false;
	}
	
	boolean optional(TokenClassEnum tokenClass){
		 if(tokenId <= tokens.size())
		 nonTerm(tokenClass);	 
		 return true;
	}
	
	boolean optional(boolean value){	 
		 return true;
	}
	
	boolean optional(Token token){
		if(tokenId <= tokens.size())
		nonTerm(token);
		return true;
	}


	public List<String> getSyntaxLog() {
		return syntaxLog;
	}


	
}
